Skip to content

MCP 2026-07-28 client over HTTP - #1667

Merged
jlouvel merged 3 commits into
3.0from
feat/mcp-2026-07-28-client
Nov 10, 2026
Merged

jlouvel merged 3 commits into
3.0from
feat/mcp-2026-07-28-client

Conversation

@jlouvel

@jlouvel jlouvel commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator

Third R6b delivery for #1640, after the server side (#1664, #1665, #1666). This adds the MCP 2026-07-28 client, over HTTP. The SDK (mcp-core 2.2.0-M2) has no modern client, only the server, so this client is built on the SDK's modern schema and JSON-RPC types.

API.

  • McpClientAdapter.setProtocolVersion(McpProtocol.MODERN_VERSION) switches the adapter to 2026-07-28. start() then runs server/discover, and fails if the server doesn't list the revision.
  • getModernClient() returns the new McpModernClient. In this mode getSdkClient() throws, since the SDK's McpAsyncClient speaks 2025-11-25 only. A stdio target fails at start() with UnsupportedOperationException; the stdio client comes next.
  • McpModernClient is reactive, on the SDK's modern types:
    • discover, listTools, callTool, listResources, listResourceTemplates, readResource, listPrompts, getPrompt;
    • listen(SubscriptionFilter), which returns a Flux of change notifications.
  • New adapter settings:
    • setModernCapabilities(): the client capabilities sent with each request, none by default;
    • setInputHandler();
    • setMaxInputRounds(), default 8.
  • McpInputHandler is a new functional interface.

Behaviour.

  • Every request carries the protocol version and the client capabilities in its _meta, as the revision requires. Ikanos's client omits the capabilities. Each request also sends MCP-Protocol-Version, Mcp-Method and Mcp-Name, base64-encoded when needed.
  • Mcp-Param headers: for tools this client has listed, the arguments their x-mcp-header annotations name are copied into Mcp-Param-* headers, numbers in plain notation. A call to an annotated tool the client hasn't listed fails with a header mismatch, as documented. These headers are worked out when the call is sent, so listTools().then(callTool(...)) works.
  • Progress: a per-call consumer receives the call's progress notifications as they stream in, before the result. A malformed notification, or a consumer that throws, is dropped rather than failing the call.
  • input_required: the input handler answers and the request is retried with its responses and the sealed requestState, up to the round limit. Without a handler, past the limit, or when the handler returns an empty Mono, the result is returned to the caller.
  • subscriptions/listen: no time limit, and its own pool, so a listen stream doesn't use the call limit's slots. The stream completes when the server ends it, for example on stop.
  • Cancellation: cancelling the returned Mono or Flux aborts the HTTP exchange, and the server cancels the handler.
  • Errors: a JSON-RPC error fails with McpException. A non-JSON or empty error reply, or an event stream that ends without a response, fails with an IOException. Responses are matched to their request's id.
  • Threads: the client's handlers and the code it hands results to run off the HTTP worker, which frees its slot as soon as the exchange ends. A handler may therefore make blocking calls, even with a call limit of 1.

Tests. McpModernClientTestCase has 12 tests against McpServerAdapter over real HTTP connectors:

  • discovery, tools, resources and prompts over HTTP/1.1 and HTTP/2, including a non-ASCII argument;
  • streamed progress;
  • input_required returned without a handler, retried by hand, and retried by a handler;
  • a listen stream receiving a published change, then completing without error when the server stops;
  • a cancelled call cancelling the tool on the server, with the next call still served;
  • Mcp-Param mirroring: refused before listing, served after it, worked out after a chained listing, and with a number like 1.0E7;
  • an error response;
  • the modern mode's limits;
  • an input handler making a blocking nested call with a call limit of 1. This test fails without the fix.

An independent review found, and this PR fixes:

  • client code ran on the interrupted worker, which still held its call slot. That broke retries at the call limit and blocking handlers, and dropped the connection after each streamed response;
  • Mcp-Param headers were worked out when the call was built, not when it was sent;
  • numbers were formatted in a way the server rejects;
  • responses weren't matched to their request's id;
  • a malformed progress notification failed the call.

mvn -pl org.restlet.ext.mcp verify: 85 tests, coverage and SpotBugs pass.

🤖 Generated with Claude Code

McpClientAdapter speaks 2026-07-28 when its protocol version is set to
it: start() runs server/discover, and getModernClient() returns an
McpModernClient on the SDK's modern types for listings, tool calls,
resource reads and prompts, with streamed progress, subscriptions/listen
and cancellation by disposing the request. An input_required result is
answered by an McpInputHandler and retried within a round limit, or
returned without one. Each request carries the protocol version and the
client capabilities in its _meta, and listed tools' x-mcp-header
arguments are mirrored into Mcp-Param headers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@jlouvel
jlouvel requested a balanced review from Copilot November 10, 2026 18:29
@jlouvel jlouvel self-assigned this Nov 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unbounded SSE buffering can exhaust heap memory, while lossy numeric ID comparisons can cause another request’s response to be accepted.

5 open issues
What changed in this PR

Adds an experimental MCP 2026-07-28 HTTP client alongside the existing legacy client, extending the modern server support delivered earlier in R6.

Changes:

  • Adds reactive discovery, tools, resources, prompts, progress notifications, input retries, and change subscriptions.
  • Adds adapter configuration and HTTP transport handling for protocol metadata, mirrored headers, cancellation, and separate call/listen pools.
  • Adds HTTP/1.1 and HTTP/2 integration tests and updates the changelog; modern stdio client support remains deferred.
File Description
org.restlet.ext.mcp/​src/​test/​java/​org/​restlet/​ext/​mcp/​McpModernClientTestCase.java Tests modern client operations and lifecycle over real HTTP connectors.
org.restlet.ext.mcp/​src/​main/​java/​org/​restlet/​ext/​mcp/​McpModernClient.java Implements reactive modern protocol operations and input retries.
org.restlet.ext.mcp/​src/​main/​java/​org/​restlet/​ext/​mcp/​McpInputHandler.java Defines the input-required response callback.
org.restlet.ext.mcp/​src/​main/​java/​org/​restlet/​ext/​mcp/​McpClientAdapter.java Adds modern protocol selection, configuration, discovery, and lifecycle management.
org.restlet.ext.mcp/​src/​main/​java/​org/​restlet/​ext/​mcp/​internal/​McpModernHttpTransport.java Implements HTTP requests, streamed responses, headers, and cancellation.
changes.md Documents modern HTTP client support and the remaining stdio limitation.

Review effort: Balanced

jlouvel and others added 2 commits November 10, 2026 13:38
Name the repeated "_meta" and "request" literals, drop the transport's
always-default listen-stream limit parameter, and keep a single throwing
call inside the tests' assertThrows lambdas.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Address the Copilot review of the modern HTTP transport:

- Read each message only once it is demanded, so a slow listen subscriber
  or progress consumer holds back the server instead of buffering without
  bound; cancelling still interrupts a worker waiting for demand.
- Compare response ids exactly, sharing the stdio side's id key, so 1.5 no
  longer matches 1 and large ids no longer wrap.
- Drop other requests' responses, and require a JSON reply to be the
  request's own response, so a listen stream ends only on its response.
- Decode streamed replies as strict UTF-8.
- Limit each streamed message in UTF-8 bytes, with SSE framing bounded on
  its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The tools cache refresh is not atomic, allowing concurrent calls to miss tool definitions and omit required Mcp-Param-* headers, causing spurious header-mismatch errors.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Prevent tool calls from observing a partially refreshed cache

org.restlet.ext.mcp/​src/​main/​java/​org/​restlet/​ext/​mcp/​McpModernClient.java:98

Refreshing the first tools page clears the shared cache before repopulating it. A concurrent callTool() subscription can run paramHeaders() in that interval, find no definition for an already-listed annotated tool, and send the call without its required Mcp-Param-* headers, causing a spurious header-mismatch error. Please publish the refreshed cache atomically (for example, replace an immutable snapshot), or synchronize both the update and lookup so calls cannot observe a partially refreshed page.

5 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues block the experimental MCP HTTP client addition.

No open issues 🎉

Review effort: Balanced

@jlouvel
jlouvel requested a balanced review from Copilot November 10, 2026 23:20
@jlouvel
jlouvel merged commit 028cafe into 3.0 Nov 10, 2026
6 checks passed
@jlouvel
jlouvel deleted the feat/mcp-2026-07-28-client branch November 10, 2026 23:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are reported for the experimental MCP HTTP client addition.

No open issues 🎉

Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants